fix(fields): emit the spec's $notContains, and keep secret out of inline edit (#2901) - #2940
Merged
Merged
Conversation
… inline edit (#2901) Two defects the #2901 audit turned up that need no design decision. **`$ncontains` was never a real operator.** The sharing-rule criteria builder emitted `{ $ncontains: v }` for "does not contain" — a token that appears zero times in `@objectstack/spec`, and that objectui's own `convertFiltersToAST` throws on while naming the correct spelling (`$notContains`) in its error message. Every such rule validated in the UI and was rejected downstream. `kvToCondition` keeps reading the old spelling so criteria saved before this still load instead of failing as "can't be represented". **`secret` was inline-editable as plaintext.** It sat in neither `EDIT_WIDGETS` nor `INLINE_EXCLUDED_FIELD_TYPES`, so the grid fell back to a plain text input — two lines below `password`, which is correctly excluded. Both are masked on read, so that input renders the mask as the value and writes it back; `secret` additionally round-trips through an encrypted store (ADR-0100), meaning the cell holds an opaque ref, not the value. Adding it tripped the exclusion-set staleness guard, which is itself mis-specified: it treats `Object.keys(fieldWidgetMap)` as the set of real field types, but spec spellings that reach a widget through the alias table (`secret` → `field:password`, `autonumber` → `field:auto_number`, …) are not keys — as index.tsx's own `resolveFormWidgetType` docs note. Made the check alias-aware so it still catches typos without rejecting real types. New `FilterConditionField.operators` test pins every builder token to a `FieldOperatorsSchema` spelling and pins the round trip; verified it fails against the old `$ncontains`. Refs #2901 Co-Authored-By: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Jul 28, 2026
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The two defects from the #2901 audit that need no design decision. Audit doc is in #2937.
$ncontainswas never a real operatorThe sharing-rule criteria builder emitted
{ $ncontains: v }for "does not contain". That token appears zero times in@objectstack/spec, and objectui's ownconvertFiltersToASTthrows on it — naming the correct spelling in its own error message:So every "does not contain" sharing rule authored in the UI validated at authoring time and was rejected downstream. Nothing caught it because the builder's operator list is hand-written and never compared against the spec vocabulary — the #2901 pattern exactly.
kvToConditionkeeps accepting$ncontainson read, so criteria saved before this still load. Dropping it would turn stored rules into "criteria can't be represented" rather than migrating them.secretwas inline-editable as plaintextsecretwas in neitherEDIT_WIDGETSnorINLINE_EXCLUDED_FIELD_TYPES, so grid inline edit fell through to DataTable's plain text input — two lines belowpassword, which is correctly excluded.Both are masked on read, so that input renders the mask as if it were the value and writes it straight back.
secretadditionally round-trips through an encrypted store (ADR-0100,data/field.zod.ts), so the cell holds an opaque ref, not the secret.The guard that blocked the fix
Adding
secrettrippedFieldEditWidget.test.ts's staleness check, which is itself mis-specified — and mis-specified in a way the audit predicted.It treats
FORM_FIELD_TYPES(Object.keys(fieldWidgetMap)) as the set of real field types. But spec spellings that reach a widget through the alias table are not keys —secret→field:password,autonumber→field:auto_number.index.tsx's ownresolveFormWidgetTypedoc comment namessecretas exactly such an alias. So the guard rejected a genuine spec field type as "stale".Made the check alias-aware: an entry is real if it is a direct widget key or the alias table maps it explicitly. Still catches typos (
secrett→ falls back tofield:text→ fails), no longer rejects real types.Verification
FieldEditWidget.test.ts,field-type-coverage.test.ts, and the newFilterConditionField.operators.test.ts— 119 tests pass.FieldOperatorsSchemaspelling, pins the round trip, and pins the$ncontainsback-compat read.$ncontainsand confirmed the new test fails withexpected [ '$ncontains' ] to deeply equal [], then restored. A guard that cannot fail is worthless.eslint: 0 errors.tsc: no new errors in the touched files.🤖 Generated with Claude Code